package util

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/md5"
	"crypto/sha256"
	"encoding/hex"
	"encoding/xml"
	"errors"
	"fmt"
	"hash"
	"io"
	"sort"
	"strings"

	pkgerr "github.com/pkg/errors"
)

//xml 转map
func XmlDecode(data string) (map[string]string, error) {
	decoder := xml.NewDecoder(strings.NewReader(data))
	result := make(map[string]string)
	key := ""
	for {
		token, err := decoder.Token() //读取一个标签或者文本内容
		if err == io.EOF {
			return result, pkgerr.Wrapf(err, "decode xml:%s meet error:%+v", data, err)
		}
		if err != nil {
			return result, pkgerr.Wrapf(err, "decode xml:%s meet error:%+v", data, err)
		}
		switch tp := token.(type) { //读取的TOKEN可以是以下三种类型：StartElement起始标签，EndElement结束标签，CharData文本内容
		case xml.StartElement:
			if tp.Name.Local != "xml" {
				key = tp.Name.Local
			}
		case xml.EndElement:
			if tp.Name.Local == "xml" {
				return result, nil
			}
		case xml.CharData: //文本数据，注意一个结束标签和另一个起始标签之间可能有空格
			data := strings.TrimSpace(string(tp))
			if len(data) != 0 {
				result[key] = data
			}
		}
	}
}

//微信支付签名
func GenMD5Sign(paramsMap map[string]interface{}, key string) string {

	//STEP 1, 对key进行升序排序.
	sortKey := make([]string, 0)
	for k, _ := range paramsMap {
		sortKey = append(sortKey, k)
	}
	sort.Strings(sortKey)
	//STEP2, 对key=value的键值对用&连接起来，略过空值
	var signStrings string
	for _, v := range sortKey {
		value := fmt.Sprintf("%v", paramsMap[v])
		if value != "" {
			signStrings = signStrings + v + "=" + value + "&"
		}
	}
	//STEP3, 在键值对的最后加上key=API_KEY
	if key != "" {
		signStrings = signStrings + "key=" + key
	}

	//STEP4, 进行MD5签名并且将所有字符转为大写.
	md5Ctx := md5.New()
	md5Ctx.Write([]byte(signStrings))
	cipherStr := md5Ctx.Sum(nil)
	upperSign := strings.ToUpper(hex.EncodeToString(cipherStr))
	return upperSign
}

// ParamSign 计算所传参数的签名
func ParamSign(p map[string]string, key string) (string, error) {
	bizKey := "&key=" + key
	str := OrderParam(p, bizKey)

	var signType string
	switch p["sign_type"] {
	case SignTypeMD5, SignTypeHMACSHA256:
		signType = p["sign_type"]
	case ``:
		signType = SignTypeMD5
	default:
		return ``, errors.New(`invalid sign_type`)
	}

	return CalculateSign(str, signType, key)
}

// OrderParam order params
func OrderParam(p map[string]string, bizKey string) (returnStr string) {
	keys := make([]string, 0, len(p))
	for k := range p {
		if k == "sign" {
			continue
		}
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var buf bytes.Buffer
	for _, k := range keys {
		if p[k] == "" {
			continue
		}
		if buf.Len() > 0 {
			buf.WriteByte('&')
		}
		buf.WriteString(k)
		buf.WriteByte('=')
		buf.WriteString(p[k])
	}
	buf.WriteString(bizKey)
	returnStr = buf.String()
	return
}

// 微信签名算法方式
const (
	SignTypeMD5        = `MD5`
	SignTypeHMACSHA256 = `HMAC-SHA256`
)

// CalculateSign 计算签名
func CalculateSign(content, signType, key string) (string, error) {
	var h hash.Hash
	if signType == SignTypeHMACSHA256 {
		h = hmac.New(sha256.New, []byte(key))
	} else {
		h = md5.New()
	}

	if _, err := h.Write([]byte(content)); err != nil {
		return ``, err
	}
	return strings.ToUpper(hex.EncodeToString(h.Sum(nil))), nil
}

// AesECBDecrypt
func AesECBDecrypt(src, key []byte, padding string) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	return ECBDecrypt(block, src, padding)
}

func ECBDecrypt(block cipher.Block, src []byte, padding string) ([]byte, error) {
	dst := make([]byte, len(src))

	mode := NewECBDecrypter(block)
	mode.CryptBlocks(dst, src)

	dst = UnPadding(padding, dst)

	return dst, nil
}

type ecb struct {
	b         cipher.Block
	blockSize int
}

func newECB(b cipher.Block) *ecb {
	return &ecb{
		b:         b,
		blockSize: b.BlockSize(),
	}
}

type ecbEncrypter ecb

// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
// mode, using the given Block.
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
	return (*ecbEncrypter)(newECB(b))
}

func (x *ecbEncrypter) BlockSize() int { return x.blockSize }

func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
	if len(src)%x.blockSize != 0 {
		panic("crypto/cipher: input not full blocks")
	}
	if len(dst) < len(src) {
		panic("crypto/cipher: output smaller than input")
	}
	for len(src) > 0 {
		x.b.Encrypt(dst, src[:x.blockSize])
		src = src[x.blockSize:]
		dst = dst[x.blockSize:]
	}
}

type ecbDecrypter ecb

// NewECBDecrypter returns a BlockMode which decrypts in electronic code book
// mode, using the given Block.
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
	return (*ecbDecrypter)(newECB(b))
}

func (x *ecbDecrypter) BlockSize() int { return x.blockSize }

func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
	if len(src)%x.blockSize != 0 {
		panic("crypto/cipher: input not full blocks")
	}
	if len(dst) < len(src) {
		panic("crypto/cipher: output smaller than input")
	}
	for len(src) > 0 {
		x.b.Decrypt(dst, src[:x.blockSize])
		src = src[x.blockSize:]
		dst = dst[x.blockSize:]
	}
}

const PKCS5_PADDING = "PKCS5"
const PKCS7_PADDING = "PKCS7"
const ZEROS_PADDING = "ZEROS"

func Padding(padding string, src []byte, blockSize int) []byte {
	switch padding {
	case PKCS5_PADDING:
		src = PKCS5Padding(src, blockSize)
	case PKCS7_PADDING:
		src = PKCS7Padding(src, blockSize)
	case ZEROS_PADDING:
		src = ZerosPadding(src, blockSize)
	}
	return src
}

func UnPadding(padding string, src []byte) []byte {
	switch padding {
	case PKCS5_PADDING:
		src = PKCS5Unpadding(src)
	case PKCS7_PADDING:
		src = PKCS7UnPadding(src)
	case ZEROS_PADDING:
		src = ZerosUnPadding(src)
	}
	return src
}

func PKCS5Padding(src []byte, blockSize int) []byte {
	return PKCS7Padding(src, blockSize)
}

func PKCS5Unpadding(src []byte) []byte {
	return PKCS7UnPadding(src)
}

func PKCS7Padding(src []byte, blockSize int) []byte {
	padding := blockSize - len(src)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(src, padtext...)
}

func PKCS7UnPadding(src []byte) []byte {
	length := len(src)
	unpadding := int(src[length-1])
	return src[:(length - unpadding)]
}

func ZerosPadding(src []byte, blockSize int) []byte {
	paddingCount := blockSize - len(src)%blockSize
	if paddingCount == 0 {
		return src
	} else {
		return append(src, bytes.Repeat([]byte{byte(0)}, paddingCount)...)
	}
}

func ZerosUnPadding(src []byte) []byte {
	for i := len(src) - 1; ; i-- {
		if src[i] != 0 {
			return src[:i+1]
		}
	}
	return nil
}
